---
title: Java内存模型
description: 解决可见性和有序性问题
---
## 什么是Java内存模型？
导致可见性的原因是缓存，导致有序性的原因是编译优化。那么，解决可见性、有序性最直接的办法就是**禁用缓存**和**编译优化**，但不能这样解决问题。


合理的方案应该是**按需禁用缓存以及编译优化**。

对于并发程序，何时禁用缓存以及编译优化只有程序员知道，所以，只需要给程序员提供按需禁用缓存和编译优化的方法即可。

Java 内存模型是个很复杂的规范，主要分为两部分，一部分是面向编写并发程序的应用开发人员，另一部分是面向 JVM 的实现人员的。

前一部分规范了 JVM 如何按需禁用缓存和编译优化的方法。具体来说，这些方法包括 **volatile**、 **synchronized** 和 **final** 三个关键字，以及六项 **Happens-Before** 规则。

## volatile

volatile 关键字并不是 Java 语言特有的，它最原始的意义就是禁用 CPU 缓存。

例如。声明一个 volatile 变量 volatile int x = 0 ， 它表达的是：告诉编译器，对这个变量的读写，不能使用 CPU 缓存，必须从内存中读取或者写入。

```java title='假设线程 A 执行 writer() 方法，按照 volatile 语义，会把变量 “v=true” 写入内存；假设线程 B 执行 reader() 方法，同样按照 volatile 语义，线程 B 会从内存中读取变量 v，如果线程 B 看到 “v == true” 时，那么线程 B 看到的变量 x 是多少呢？'
class VolatileExample {
  int x = 0;
  volatile boolean v = false;
  public void writer() {
    x = 42;
    v = true;
  }
  public void reader() {
    if (v == true) {
      // 这里x会是多少呢？
    }
  }
}
```
直觉上看，应该是 42，实际上这个要看 Java 的版本，如果在 低于 1.5 版本上运行，x 可能是 42 ，也有可能是 0 ；如果在 1.5 以上的版本运行， x 就是等于 42 。

变量 x 可能被 CPU 缓存而导致的可见性问题，在 1.5 版本之后已经得到了解决。Java 内存模型在 1.5 版本对 volatile 语义进行了增强。

## Happens—Before 规则
Happens-Before 要表达式的是**前面一个操作的结果对后续操作的结果是可见的。**

Happens——Before约束了编译器的优化行为，虽允许编译器优化，但是要求编译器优化后一定遵守 Happens-Before 规则。

Happens——Before 规则和程序员相关的一共六项，都是关于可见性的。

### 1. 程序的顺序性规则
在一个线程中，按照程序顺序，前面的操作 Happens-Before 于后续的任意操作。

比如前面的示例代码，按照程序的顺序，x = 42 Happens-Before v = true。


### 2. volatile 变量规则
对一个 volatile 变量的写操作， Happens-Before 于后续对这个变量的读操作。

这条需要和第三条规则关联使用。
### 3. 传递性
如果 A Happens—Before B ，B Happens-Before C,那么 A Happens—Before C 。



![b1fa541e98c74bc2a033d9ac5ae7fbe1](https://img.wkq.pub/pic/b1fa541e98c74bc2a033d9ac5ae7fbe1.webp)

从图中，可以看到：

1. x = 42 Happens-Before 写变量 v =true  
2. 写变量 v = true  Happens-Before 读变量 v = true;
3. 得到结论 x = 42 Happens-Before 读变量 v =true ;

这就意味着 线程 B 能看到 x == 42。

:::tip

这就是 1.5 版本对 volatile 语义的增强，1.5 版本的并发工具包（java.util.concurrent）就是靠 volatile 语义来搞定可见性。

:::



### 4. 管程中锁的规则

对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。

:::tip

管程是一种通用的同步原语，在 Java 中指的就是 synchronized ,synchronized 是 Java 里对管程的实现。

:::

管程中的锁在 Java 里是隐式实现的。

```java title='在进入同步代码块之前自动加锁，而在代码块执行完会自动释放锁，加锁与释放锁都是编译器帮我们实现的'
synchronized(this){//此处自动加锁
    //x 是共享变量，初始值是 10 
    if(this.x < 12){
        this.x = 12;
    }
}//此处自动解锁
```

假设 x 的初始值是 10 ，线程 A 执行完代码块后 x 的值变成 12 （执行完自动释放锁），线程 B 进入代码块时，能够看到 线程 A 对 x 的写操作，也就是线程 B 能够看到 x =12 。

### 5. 线程 start() 规则

主线程 A 启动子线程 B 后，子线程 B 能够看到主线程在启动前的操作（在线程 A 中启动 线程 B ，start()  操作 Happends Before 线程 B 中的任意操作）。

```java
Thread B = new Thread(()->{
    //主线程调用 B.start() 之前，所有对共享变量的修改，此处皆可见 
});
//此处对共享变量 var进行修改
var = 77;
B.start();
```

### 6. 线程 join() 规则

主线程 A 等待子线程 B 完成（主线程 A 通过调用子线程 B 的 join() 方法实现），当子线程 B 完成后（主线程 A 中的 join() 方法返回），主线程能够看到子线程的操作（对共享变量的修改）。



如果在线程 A 中，调用线程  B 的 join()方法，线程 B 中的任意操作 Happens-Before join() 方法的返回。

```java
Thread B = new Thread(()->{
    //此处对共享变量进行修改
    var = 66;
});

B.start();
B.join();
//主线程所有对共享变量的修改，在主线程调用 B.join() 之后都是可见的。
```

## final

volatile 的作用是禁用缓存以及编译优化。

final 是告诉编译器，**这个变量生而不变，可以使劲优化。**

